Skip to main content

深入理解 Promise 实现

· 8 min read
fish

网上有很多 Promise 实现方式,看了都不是特别理解。 这里以一种更简单的形式一步一步去理解/实现它。这里仅涉及 Promise 构造函数和 then 方法的实现

首先构造一个最基本的 Promise 类

// version_1
class Promise {
callbacks = [];
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
this.callbacks.forEach(callback => callback(value));
}
}

// test
new Promise(resolve => {
setTimeout(() => {
console.log('await 2s');
resolve('ok');
}, 2000);
}).then((res) => {
console.log('then', res);
})
  1. Promise 构造函数会立即执行用户传入的函数 executor,并且把 _resolve 方法作为 executor 的参数,传给用户处理
  2. 调用 then 方法(同步),将 onFulfilled 放入callbacks队列,其实也就是注册回调函数,类似于观察者模式。
  3. executor 模拟了异步,这里是过2s后执行 resolve,对应触发 _resolve 内的 callbacks

.then(onFulfilled) 为何需要用一个数组存放?

then 方法可以调用多次,注册的多个onFulfilled,并且这些 onFulfilled callbacks 会在异步操作完成(执行resolve)后根据添加的顺序依次执行

// then 注册多个 onFulfilled 回调
const p = new Promise(resolve => {
setTimeout(() => {
console.log('await 2s');
resolve('ok');
}, 2000);
});

p.then(res => console.log('then1', res));
p.then(res => console.log('then2', res));
p.then(res => console.log('then3', res));

异步执行处理 setTimeout vs status

上面 Promise 的实现存在一个问题:如果传入的 executor 不是一个异步函数,resolve直接同步执行,这时 callbacks 还是空数组, 导致后面 then 方法注册的 onFulfilled 回调就不会执行(resolve 比 then 注册先执行)

// 同步执行 resolve
new Promise(resolve => {
console.log('同步执行');
resolve('同步执行');
}).then(res => {
console.log('then', res);
})

我们知道 then 中的回调总是通过异步执行的,我们可以在 resolve 中加入 setTimeout,将 callbacks 的执行时机放置到JS消息队列,这样 then方法的 onFulfilled 会先完成注册,再执行消息队列的 resolve

// version_2
class Promise {
callbacks = [];
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
setTimeout(() => {
this.callbacks.forEach(callback => callback(value));
})
}
}

但是这样仍然有问题,如果我们延迟给 then 注册回调,这些回调也都无法执行。因为 还是 resolve 先执行完了,之后注册的回调就无法执行了。

const p = new Promise(resolve => {
console.log('同步执行');
resolve('同步执行');
})

setTimeout(() => {
p.then(res => {
console.log('then', res); // never execute
})
});

可以看出 setTimeout 是无法保证 then 注册的 onFulfilled 正确执行的,所以这里必须加入状态机制(pending、fulfilled、rejected),且状态只能由 pending 转换为解决或拒绝。

// version_3:增加状态机制
class Promise {
callbacks = [];
status = 'pending';
value = undefined;
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
if (this.status === 'pending') {
this.callbacks.push(onFulfilled);
} else {
onFulfilled(this.value);
}
}
_resolve(value) {
this.status = 'fulfilled';
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}

当增加了状态后,setTimeout 就可以去掉了,状态机制让注册的回调总是能正确工作。

  • 当 resolve 同步执行时,立即执行 resolve,将 status 设置为 fulfilled ,并把 value 的值存起来, 在此之后调用 then 添加的新回调,都会立即执行
  • 当 resolve 异步执行时,pending 状态执行 then 会添加回调函数, 等到 resolve 执行时,回调函数会全部被执行。

then的链式调用

链式调用我们可能很直接想到 then 方法中返回 this,这样 Promise 实例就可以多次调用 then 方法,但因为是同一个实例,调用再多次 then 也只能返回相同的一个结果。而我们希望的链式调用应该是这样的:

new Promise(resolve => {
resolve(1)
}).then(res => res + 2) // 1 + 2 = 3
.then(res => res + 3) // 3 + 3 = 6
.then(res => console.log(res)); // expected 6

每个 then 注册的 onFulfilled 都返回不同结果,并把结果传给下一个 onFulfilled 的参数,所以 then 需要返回一个新的 Promise 实例

// version_4:then 的链式调用
class Promise {
callbacks = [];
status = 'pending';
value = undefined;
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
return new Promise(resolveNext => {
const fulfilled = (value) => {
const results = onFulfilled(value); // 执行 onFulfilled
resolveNext(results); // 再执行 resolveNext
}
if (this.status === 'pending') {
this.callbacks.push(fulfilled);
} else {
fulfilled(this.value);
}
})
}
_resolve(value) {
this.status = 'fulfilled';
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}

这样一个 Promise 就基本实现了,我们可以看到:

  • then 方法中,创建并返回了新的 Promise 实例,这是串行 Promise 的基础
  • 我们把 then 方法传入的 形参 onFulfilled 以及创建新 Promise 实例时传入的 resolveNext 合成一个 新函数 fulfilled,这是衔接当前 Promise 和后邻 Promise 的关键所在

处理返回 Promise 类型的回调

这里还有一种特殊的情况:

  • resolve 方法传入的参数为一个 Promise 对象时
  • onFulfilled 方法返回一个 Promise 对象时

这时我们只需用 res instanceof Promise 判断处理下

// version_5:Promise 参数处理
class Promise {
callbacks = [];
status = 'pending';
value = undefined;

constructor(executor) {
executor(this._resolve.bind(this));
}

then(onFulfilled) {
return new Promise(resolveNext => {
const fulfilled = (value) => {
const results = onFulfilled(value);
if (results instanceof Promise) {
// 如果当前回调函数返回Promise对象,必须等待其状态改变后在执行下一个回调
results.then(resolveNext);
} else {
// 否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
resolveNext(results);
}
}
if (this.status === 'pending') {
this.callbacks.push(fulfilled);
} else {
fulfilled(this.value);
}
})
}

_resolve(value) {
this.status = 'fulfilled';
/**
* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
* 当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (value instanceof Promise) {
value.then(nextValue => {
this.value = nextValue;
this.callbacks.forEach(callback => callback(value));
})
} else {
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}
}

拓展练习(面试题)

尝试实现下面函数 LazyMan 的功能

LazyMan('Tony');
// Hi I am Tony

LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

参考答案from Daily-Interview-Question